fix(channel): Prevent leaked SocketMap references during reinitializa… - #3433
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes brpc::Channel reinitialization for direct (single-server) channels by ensuring the exact SocketMapKey used during initialization is preserved and properly released, preventing leaked SocketMap references and avoiding partial state overwrites when reinitialization fails.
Changes:
- Track and remove the precise
SocketMapKeyfor the active single-server channel state to prevent staleSocketMapreferences after reinit/destruction. - Rework initialization to build
ChannelOptions/protocol function pointers locally and only commit to theChannelon success, keeping prior state intact on failure. - Add unit tests covering repeated direct initialization and failed reinitialization behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/brpc_channel_unittest.cpp | Adds regression tests for socket-map reference release on reinit and state preservation on failed reinit. |
| src/brpc/channel.h | Introduces internal single-server state storage (unique_ptr) and a reset helper for single-server lifecycle. |
| src/brpc/channel.cpp | Implements stored SocketMapKey lifecycle management and commits init state only after success to avoid partial overwrites. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
416cdb9 to
9f169d0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/brpc/channel.cpp:497
Init(ns_url, lb_name, ...)commits_optionsand protocol function pointers before validatingclient_hostand beforelb->Init(). Ifclient_hostis invalid orlb->Init()fails,Init()returns-1but the channel’s options/protocol callbacks have already been overwritten while_scheme/_service_name/_lbremain unchanged. This creates a partially-updated channel configuration after a failed reinit.
Suggested fix: keep the parsed URL pieces and initialized_options local, and only assign _options/callbacks and swap _scheme/_service_name after lb->Init() succeeds (similar to how you defer ResetSingleServer() today).
_options = initialized_options.options;
_serialize_request = initialized_options.serialize_request;
_pack_request = initialized_options.pack_request;
_get_method_name = initialized_options.get_method_name;
_preferred_index = initialized_options.preferred_index;
src/brpc/channel.cpp:416
InitSingle()assigns_optionsand protocol function pointers before validatingserver_addr_and_port.port/client_hostand before the SocketMap insert+commit block. If any of the subsequent checks fail and the function returns-1, the channel keeps using the previously-initialized socket-map entry but its options are partially overwritten, leaving the channel in an inconsistent state after a failed reinitialization (contrary to the PR description’s “keep prior state intact on failure”).
Consider keeping initialized_options purely local and only committing _options/_serialize_request/etc after all validation succeeds and you’re ready to ResetSingleServer() + swap in the new state (or add a scoped rollback guard to restore the previous members on every early-return).
This issue also appears on line 493 of the same file.
_options = initialized_options.options;
_serialize_request = initialized_options.serialize_request;
_pack_request = initialized_options.pack_request;
_get_method_name = initialized_options.get_method_name;
_preferred_index = initialized_options.preferred_index;
test/brpc_channel_unittest.cpp:2353
- This test currently asserts that
channel.options().client_hostequals the invalidclient_hostfrom a failedInit()call. If the intent is to keep the prior channel state intact when reinitialization fails, options should remain unchanged after the failed init.
Capturing the original client_host and asserting it remains the same also avoids locking in inconsistent runtime behavior where options() no longer matches the socket that is actually in use.
EXPECT_EQ(endpoint, channel._server_address);
EXPECT_EQ(original_id, channel._server_id);
EXPECT_EQ(invalid_options.client_host, channel.options().client_host);
EXPECT_TRUE(channel.SingleServer());
|
LGTM |
chenBright
left a comment
There was a problem hiding this comment.
Instead of managing the SocketMap key lifecycle for re-init, I'd suggest simply rejecting re-initialization once Init() has succeeded. A Channel that's initialized exactly once inserts exactly one socket entry and never mutates _options afterward, so the signature recomputed in ~Channel() always matches the insertion key and the reference stays balanced — no extra state needed. Failed inits before the first success can still be retried.
Thanks for the suggestion! That makes total sense. Rejecting re-initialization after a successful Init() keeps Channel simple without extra heap allocation or state tracking, while still allowing failed Init() attempts to be retried. I will update the PR to follow this approach. |
9f169d0 to
c0aaf32
Compare
|
Still following this PR? @chenBright |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
test/brpc_channel_unittest.cpp:2357
- Same issue as above: this only checks that SocketMapFind(SocketMapKey(endpoint)) fails after destruction, but does not first confirm that this key was actually present during the successful Init. Adding an in-scope ASSERT_EQ(0, SocketMapFind(SocketMapKey(endpoint), &id)) (and optionally id == channel._server_id) would ensure the test is validating removal of the correct key rather than passing due to a key/signature mismatch.
brpc::SocketId id;
EXPECT_NE(0, brpc::SocketMapFind(brpc::SocketMapKey(endpoint), &id));
}
| ASSERT_EQ(0, channel.Init(first_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init(first_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init(second_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init("unknown://unknown", "rr", NULL)); | ||
| } |
There was a problem hiding this comment.
The code and test had already switched to rejecting reinitialization after a successful Init(), but the GitHub PR description was not updated accordingly, which caused the inconsistency.
| { | ||
| brpc::Channel channel; | ||
| ASSERT_EQ(0, channel.Init(first_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init(first_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init(second_endpoint, NULL)); | ||
| ASSERT_EQ(-1, channel.Init("unknown://unknown", "rr", NULL)); | ||
| } | ||
|
|
||
| brpc::SocketId id; | ||
| EXPECT_NE(0, brpc::SocketMapFind(brpc::SocketMapKey(first_endpoint), &id)); | ||
| EXPECT_NE(0, brpc::SocketMapFind(brpc::SocketMapKey(second_endpoint), &id)); |
| if (_server_id != INVALID_SOCKET_ID || _lb != NULL) { | ||
| LOG(ERROR) << "Channel=" << this << " has already been initialized"; | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
It has been resolved previously.
e75913f to
1a4dfa4
Compare
|
Turn multiple commits into one |
…lanced Reject re-initialization once Channel::Init() has succeeded. This ensures a Channel instance only inserts into SocketMap at most once and preserves its options and signature intact, guaranteeing that ~Channel() always balances the insertion without requiring extra state tracking. Failed inits before the first successful initialization can still be retried.
1a4dfa4 to
246a1c7
Compare
|
LGTM |
@wwbmmm You can choose "Squash and merge" to merge commits during the merge process, which makes the commit history clearer. |
OK |
What problem does this PR solve?
Problem Summary:
A direct Channel could be initialized more than once. Each successful initialization inserted a SocketMap entry, while destruction released only the key derived from
the final configuration, leaving earlier entries unbalanced.
What is changed and the side effects?
Changed:
Side effects: